package com.puzzlebazar.client.squaregrid.view;

import com.puzzlebazar.client.squaregrid.model.SGModel;
import com.puzzlebazar.client.util.GridValidityImpl;
import com.puzzlebazar.client.util.LayeredPanel;
import com.puzzlebazar.client.util.Observable;
import com.puzzlebazar.client.util.Observer;
import com.puzzlebazar.client.util.Recti;
import com.puzzlebazar.client.util.Vec2i;

public abstract class SGViewImpl extends LayeredPanel implements SGView, Observer {

	final private SGModel squareGrid;
	private final Vec2i outerSize;
	private final Recti innerRect;
	private final Vec2i gridSize;
    private int bleed;
    
	/**
	 * Create a new view for a standard square grid
	 * Derived class should call addObserver on the squareGrid
	 * 
	 * @param squareGrid The square grid model to view
	 * @param outerSizeX The width of the view panel, including bleed (in pixels)
	 * @param outerSizeY The height of the view panel, including bleed (in pixels)
	 * @param bleed The amount of bleed (in pixels)
	 * @throws Exception 
	 */
	public SGViewImpl(SGModel squareGrid, final Vec2i outerSize, int bleed) throws Exception {
		this.squareGrid = squareGrid;
		this.bleed = bleed;
		this.outerSize = new Vec2i( outerSize );
		gridSize = new Vec2i( squareGrid.getSize() );
		innerRect = getInnerRectFromOuterSize( outerSize );
	}

	@Override
	public void setOuterSize( Vec2i outerSize ) throws Exception {

		if( outerSize.equals( this.outerSize ) )
			return;
		
		Recti newInnerRect = getInnerRectFromOuterSize( outerSize );
		this.outerSize.copy( outerSize );
		innerRect.copy( newInnerRect );
		resize();
	}

	@Override
	public void setBleed( int bleed ) throws Exception {
    	if( bleed < 0 ) bleed = 0;
    	if( bleed == this.bleed ) return;
    	// Maintain outerSize;
    	int delta = (this.bleed - bleed) * 2;
    	innerRect.w += delta;
    	innerRect.h += delta;
    	this.bleed = bleed;
    	// Call resize even if outersize doesn't change, so that widget interested
    	// in the inner size can query it
		resize();  
    }
		
	public Vec2i getInnerSize() {
		return new Vec2i( innerRect.w, innerRect.h );
	}


	@Override
	public Recti getCellPositionAndSize( Vec2i loc ) throws Exception {
		if( !squareGrid.isValidCell( loc ) ) 
			throw new Exception( "Invalid cell" );
		
		Recti result = new Recti(0,0,0,0);
		result.x = getPixelPosX(loc.x) + 1;
		result.y = getPixelPosY(loc.y) + 1;
		result.w = getPixelPosX(loc.x+1) - result.x;
		result.h = getPixelPosY(loc.y+1) - result.y;
		
		if ( result.w < 0 ) result.w = 0;
		if ( result.h < 0 ) result.h = 0;
		
		return result;
	}

	@Override
	public Vec2i getVertexPosition( Vec2i loc ) throws Exception {
		if( !squareGrid.isValidVertex( loc ) )	throw new Exception( "Invalid vertex" );
		
		Vec2i result = new Vec2i(0,0);
		result.x = getPixelPosX(loc.x);
		result.y = getPixelPosY(loc.y);
		
		return result;
	}

	@Override
	public Vec2i getNearestVertex( Vec2i pixel  ) {

		Vec2i result = new Vec2i(0,0);
		result.x = getLocationX(pixel.x);
		result.y = getLocationY(pixel.y);
		
		return result;
	}

	@Override
	public final Recti getInnerRect() {
		return innerRect;
	}

	@Override
	public final Vec2i getGridSize() {
		return gridSize;
	}

    /**
     * Computes the inner rectangle based on the outer size and the bleed.
     * This function ensures that the aspect ratio matches that of the model.
     * 
     * @param outerSize The maximal outer size, in pixels
     * @return The rectangle corresponding to the inner portion of the grid
     * @throws Exception 
     */
    private Recti getInnerRectFromOuterSize(Vec2i outerSize) throws Exception {

    	Recti result = new Recti(
    			bleed,
    			bleed,
    			outerSize.x-2*bleed,
    			outerSize.y-2*bleed
    	);
    	
		if( result.w <= 0 || result.h <= 0 )
			throw new Exception( "Invalid inner size!" );
		
		if( gridSize.x == 0 || gridSize.y == 0 ) 
			return result;
		
		// Adjust ratio to match model grid size
		int desiredWidth = (result.h * gridSize.x) / gridSize.y;
		if( desiredWidth <= result.w && desiredWidth > 0 ) {
			result.x += (result.w-desiredWidth)/2;
			result.w = desiredWidth;
		}
		else {
			int desiredHeight = (result.w * gridSize.y) / gridSize.x;
			if( desiredHeight <= result.h && desiredHeight > 0  ) {
				result.y += (result.h-desiredHeight)/2;
				result.h = desiredHeight;
			}
		}

		return result;
	}
    
	
	/**
	 * Gets the X position, in pixel, of the grid line to the left of the cells in 
	 * column locX. Note that locX and be 1 more than the number of columns in order
	 * to identify the position of the leftmost vertical grid line.
	 * 
	 * @param locX Index of the column
	 * @return Position, in pixel, of the vertical line before this column
	 */
	private int getPixelPosX(int locX) {
		return getPixelPos( locX, innerRect.x, innerRect.w, gridSize.x + 1 );
	}

	/**
	 * Gets the Y position, in pixel, of the grid line to the top of the cells in 
	 * row locY. Note that locY and be 1 more than the number of rows in order
	 * to identify the position of the bottom most horizontal grid line.
	 * 
	 * @param locY Index of the row
	 * @return Position, in pixel, of the horizontal line before this row
	 */
	private int getPixelPosY(int locY) {
		return getPixelPos( locY, innerRect.y, innerRect.h, gridSize.y + 1 );
	}
	
	/**
	 * Gets the row index of the vertex closest to the passed X position
	 * 
	 * @param pixelX X position, in pixel
	 * @return Row index of the closest vertex
	 */
	private int getLocationX(int pixelX) {
		return getLocation( pixelX, innerRect.x, innerRect.w, gridSize.x + 1 );
	}

	/**
	 * Gets the row index of the vertex closest to the passed Y position
	 * 
	 * @param pixelY Y position, in pixel
	 * @return Row index of the closest vertex
	 */
	private int getLocationY(int pixelY) {
		return getLocation( pixelY, innerRect.y, innerRect.h, gridSize.y + 1 );
	}
	
	
	/**
	 * A simple helper function to return the position of a location (X or Y) in pixel
	 * provided the number of lines or columns and the total size in pixel (in X or Y)
	 * 
	 * @param loc X or Y location of the grid cell
	 * @param pixelPos X or Y origin of the view 
	 * @param pixelSize X or Y size of the view
	 * @param nbLines Number of lines (or columns)
	 * @return
	 */
	private int getPixelPos(int loc, int pixelPos, int pixelSize, int nbLines) {
		return loc * ( pixelSize - 1 ) / ( nbLines - 1 ) + pixelPos;
	}

	/**
	 * Gets the location of the cell (X or Y) provided a position in pixel (X or Y)
	 * provided the number of lines or columns and the total size in pixel (in X or Y)
	 * 
	 * @param pixel X or Y position, in pixel, within the square grid
	 * @param pixelPos X or Y origin of the view 
	 * @param pixelSize X or Y size of the view
	 * @param nbLines Number of lines (or columns)
	 * @return
	 */
	private int getLocation(int pixel, int pixelPos, int pixelSize, int nbLines) {
		int result = (int)(((float)pixel - pixelPos) * ( nbLines - 1 ) / ( (float)pixelSize - 1 )+0.5f);
		if (result < 0) return 0;
		if (result > nbLines) return nbLines;
		return result;
	}
	public void update(Observable observable, Object arg) {

		if( gridSize.equals( squareGrid.getSize() ) )
			return;
		
		gridSize.copy( squareGrid.getSize() );
		
		// We just got a resize event, cell locations have changed.
		// Also, innerSize may have to change to respect aspect ratio.
		// Notify subwidgets via a resize, even if outerSize hasn't changed,
		// so that they can update if desired.
		try {
			Recti newInnerRect;
			newInnerRect = getInnerRectFromOuterSize( outerSize );
			innerRect.copy( newInnerRect );
		} catch (Exception exception) {
			exception.printStackTrace();
		}
		resize();
	}

	/**
	 * Ensures the size of this widget matches the outer size
	 */
	private void resize() {
		setPixelSize(outerSize.x, outerSize.y);
	}

	@Override
	public SGModel getPuzzleModel() {
		return squareGrid;
	}
	
	
	@Override
	public boolean isValidCell(Vec2i loc) {
		return GridValidityImpl.isValidCell(loc, gridSize);
	}

	@Override
	public boolean isValidVertex(Vec2i loc) {
		return GridValidityImpl.isValidVertex(loc, gridSize);
	}	

	@Override
	public boolean isValidVerticalEdge(Vec2i loc) {
		return GridValidityImpl.isValidVerticalEdge(loc, gridSize);
	}	

	@Override
	public boolean isValidHorizontalEdge(Vec2i loc) {
		return GridValidityImpl.isValidHorizontalEdge(loc, gridSize);
	}

	@Override
	public boolean isValidRect(Recti rect) {
		return GridValidityImpl.isValidRect(rect, gridSize);
	}

	@Override
	public void release() {
		squareGrid.deleteObserver( this );
	}

}
